<?php
/**
 * php GEM Project
 * 
 * LICENSE
 * 
 * PHP System Architecture Enabling Rapid Application Deployment and Scalability
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @copyright Copyright (C) 2010 Zeletron LLC (http://zeletron.com)
 * @license http://gem.zeletron.com/license
 */

/*
 * GEM/Model/Domain/ObjectManager.php
 * 
 * The ObjectManager follows the Unit of Work pattern, as described by
 * Martin Fowler. The ObjectManager tracks domain objects, and eliminates
 * redundant database queries and limits database transactions until the
 * commit() method is called.
 * @link http://martinfowler.com/eaaCatalog/unitOfWork.html
 * 
 * @author David Joly
 * @link http://zeletron.com
 * 
 * @category GEM
 * @package GEM_Model
 * @subpackage GEM_Model_Domain
 * 
 */

class GEM_Model_Domain_ObjectManager
{
	/**
	 * Singleton Instance
	 * 
	 * @var GEM_Model_Domain_ObjectManager
	 */
	private static $_instance;
	
	/**
	 * Stores GEM_Model_Domain_Object objects.
	 * 
	 * @var array
	 */
	private $_all = array();
	
	/**
	 * GEM_Model_Domain_Object objects that have been modified
	 * since instantiation are marked as "dirty" and tracked in
	 * the $_dirty array.
	 * 
	 * @var array
	 */
	private $_dirty = array();
	
	/**
	 * Newly created GEM_Model_Domain_Object objects
	 * are marked as new and tracked in the $_new array.
	 * 
	 * @var array
	 */
	private $_new = array();
	
	/**
	 * GEM_Model_Domain_Object objects that have been marked
	 * from deletion are tracked in the $_deleted array.
	 * 
	 * @var array
	 */
	private $_deleted = array();
	
	/** Singleton Constructor */
	private function __construct(){}
	
	/**
	 * @return GEM_Model_Domain_ObjectManager
	 */
	public static function instance()
	{
		if(!isset(self::$_instance)){
			self::$_instance = new self();
		}
		return self::$_instance;
	}
	
	/**
	 * Returns a unique identifier for an object.
	 * 
	 * @param GEM_Model_Domain_Object $obj
	 * @return string
	 */
	private function _key(GEM_Model_Domain_Object $obj)
	{
		$key = get_class($obj) . '.' . $obj->getId(); 
		return $key;
	}
	
	/**
	 * Check $_all array to see if an object exists or not. If the
	 * object exists, it is returned. Otherwise null is returned.
	 * This can be used by a mapper or object factory to avoid creating 
	 * an object that already exists.
	 * 
	 * @param string $classname
	 * @param int $id
	 * @return GEM_Model_Domain_Object
	 */
	public static function exists($classname, $id)
	{
		$objectManager = self::instance();
		$key = "$classname.$id";
		if(array_key_exists($key,$objectManager->_all)){
			return $objectManager->_all[$key];
		}
		return null;
	}
	
	/**
	 * This method can be used to retrieve objects that you
	 * don't know the id. It will iterate through the $_all
	 * array, attempting to match an object with the same class
	 * name and specified property values. This can be used by
	 * more specific finder methods that search for properties
	 * other than id.
	 * 
	 * @param string $classname
	 * @param array $properties | array('property' => 'value')
	 * @return GEM_Model_Domain_Object
	 */
	public static function deepSearch($classname,array $properties = array()){
		if(!class_exists($classname))
			throw new GEM_Model_Domain_Exception("$classname not found.");
		$objectManager = self::instance();
		foreach($objectManager->_all as $obj){
			if($classname == get_class($obj)){
				//Class match found, do the properties match?
				$match = true;
				foreach($properties as $property => $value){
					$getter = "get".ucfirst($property);
					if(!method_exists($classname,$getter))
						throw new GEM_Model_Domain_Exception("Method $getter() not found in $classname.");
					if($obj->$getter() != $value) {
						$match = false;
						break;
					}
				}
				if($match) return $obj; 
			}
		}
		//No match found.
		return null;
	}
	
	/**
	 * Adds a GEM_Model_Domain_Object $obj to $_all array.
	 * 
	 * @param GEM_Model_Domain_Object $obj
	 * @return void
	 */
	public static function register(GEM_Model_Domain_Object $obj)
	{
		$objectManager = self::instance();
		$objectManager->_all[$objectManager->_key($obj)] = $obj;
	}
	
	/**
	 * Adds a GEM_Model_Domain_Object $obj to $_deleted array to mark
	 * for deletion.
	 * 
	 * @param GEM_Model_Domain_Object $obj
	 * @return void
	 */
	public static function registerDeleted(GEM_Model_Domain_Object $obj)
	{
		$objectManager = self::instance();
		$objectManager->_deleted[$objectManager->_key($obj)] = $obj;
	}
	
	/**
	 * Adds a GEM_Model_Domain_Object $obj to $_dirty array to mark
	 * for possible update operation.
	 * 
	 * @param GEM_Model_Domain_Object $obj
	 * @return void
	 */
	public static function registerDirty(GEM_Model_Domain_Object $obj)
	{
		$objectManager = self::instance();
		if(!in_array($obj,$objectManager->_new, true)){
			$objectManager->_dirty[$objectManager->_key($obj)] = $obj;
		}
	}
	
	/**
	 * Adds a GEM_Model_Domain_Object $obj to $_new array to mark
	 * for insert operation.
	 * 
	 * @param GEM_Model_Domain_Object $obj
	 * @return void
	 */
	public static function registerNew(GEM_Model_Domain_Object $obj)
	{
		$objectManager = self::instance();
		$objectManager->_new[] = $obj;
	}
	
	/**
	 * Removes a GEM_Model_Domain_Object $obj from $_dirty and $_new arrays.
	 * 
	 * @param GEM_Model_Domain_Object $obj
	 * @return void
	 */
	public static function registerClean(GEM_Model_Domain_Object $obj)
	{
		$objectManager = self::instance();
		
		unset($objectManager->_dirty[$objectManager->_key($obj)]);
		if(in_array($obj,$objectManager->_new, true)){
			//Remove $obj from $_new array...
			$new = array();
			foreach($objectManager->_new as $newObject){
				if(!($newObject === $obj)){
					$new[] = $newObject;
				}
			}
			//Reset $_new array
			$objectManager->_new = $new;
		}
	}
	
	/**
	 * Iterates through each object marked for a database 
	 * operation, retreives its corresponding data mapper
	 * or persistence class and orders the marked operations
	 * to be completed.
	 * 
	 * Update 06/27/2010:
	 * This method is modified to offer very basic transaction
	 * rollback functionality.
	 * 
	 * @see GEM_Model_Domain_Object
	 * @return boolean
	 */
	public function commit()
	{
		Zend_Registry::get('pdo')->beginTransaction();
		try {
			//Insert New Objects
			foreach($this->_new as $obj){
				$obj->finder()->insert($obj);
			}
			//Delete deleted objects
			foreach($this->_deleted as $obj){
				$obj->finder()->delete($obj);
			}
			//Update 'dirty' objects
			foreach($this->_dirty as $obj){
				$obj->finder()->update($obj);
			}
		} catch (PDOException $e){
			Zend_Registry::get('pdo')->rollBack();
			Zend_Registry::get('logger')->log($e->getTraceAsString(),Zend_Log::DEBUG);
			$this->clear();
			return false;
		}
		//Clear operation tracking arrays
		Zend_Registry::get('pdo')->commit();
		$this->clear();
		return true;
	}
	
	/**
	 * Clears operation tracking arrays.
	 *
	 * @return void
	 */
	public function clear()
	{
		$this->_deleted = array();
		$this->_dirty = array();
		$this->_new = array();
	}
	
	/**
	 * @TODO Apparently most implementations of the
	 * Unit of Work pattern don't rollback database
	 * transaction due to the complexity of the code
	 * involved to do so. Rather, they simply reset the
	 * Unit of Work object. According to Fowler, clear()
	 * is probably a better method name. If implementing
	 * a true rollback, use this method.
	 * 
	 * @return void
	 */
	public function rollback()
	{
		
	}
}